#!/usr/bin/python

from email.utils import formatdate
import hmac
import hashlib
import base64
import ConfigParser
import os.path
import sys
import httplib2
import xml.dom.minidom
import json
import time

class hmacMisMatch(Exception):
    pass

class http200(Exception):
    pass

class ChangePending(Exception):
    pass

class LocalIPFetchFail(Exception):
    pass

class callR53:
    endpoint = None
    key = None
    id = None
    hostedZone = None
    
    def __init__(self, callerInfo):
        self.endpoint = "https://route53.amazonaws.com/{}/".format(callerInfo["r53API"])
        self.key = callerInfo["key"]
        self.id = callerInfo["id"]
        
    def callR53(self, req, postData = None):
        timeStamp = formatdate(timeval=None, localtime=False, usegmt=True)
        amznAuth = "AWS3-HTTPS AWSAccessKeyId={},Algorithm=HmacSHA256,Signature={}".format(self.id, base64.encodestring(hmac.new(self.key, timeStamp, hashlib.sha256).digest()))
        
        http = httplib2.Http()
        if postData is None:
            response, content = http.request(self.endpoint + req, headers={'Date': timeStamp, 'X-Amzn-Authorization': amznAuth})
        else:
            response, content = http.request(self.endpoint + req, method="POST", body=postData, headers={'Date': timeStamp, 'X-Amzn-Authorization': amznAuth})
        
        if response.status != 200:
            try:
                dom = xml.dom.minidom.parseString(content)
                errorType = dom.getElementsByTagName("Type").item(0).firstChild.data
                errorCode = dom.getElementsByTagName("Code").item(0).firstChild.data
                errorMessage = dom.getElementsByTagName("Message").item(0).firstChild.data
                errorRequestID = dom.getElementsByTagName("RequestId").item(0).firstChild.data
                print "Fatal Error\n\t{}: {}".format(errorCode, errorMessage)
            except:
                print "Fatal Error:\n\t{}".format(content)
            raise SystemExit
    
        return content
        
    def getHostedZones(self):
        xmlData = self.callR53("hostedzone")
        zones = []
        dom = xml.dom.minidom.parseString(xmlData)
        nodeName, tree = parse_element(dom)
        for zone in tree["ListHostedZonesResponse"]["HostedZones"]["HostedZone"]:
            name = zone["Name"]["#text"]["data"]
            id = zone["Id"]["#text"]["data"]
            callerReference = zone["CallerReference"]["#text"]["data"]    
            resourceRecordSetCount = zone["ResourceRecordSetCount"]["#text"]["data"]
            comment = zone["Config"]["Comment"]["#text"]["data"]
            zones.append({name: {"name": name, "id": id, "callerReference": callerReference, "resourceRecordSetCount": resourceRecordSetCount, "comment": comment}})
        return zones
    
    def fetchZoneID(self, zoneName):
        zones = self.getHostedZones()
        for zone in zones:
            if zoneName in zone:
                return zone[zoneName]["id"]
        return None
    
    def getRRs(self):
        resourceRecords = []
        xmlData = self.callR53(self.hostedZone + "/rrset?")
        dom = xml.dom.minidom.parseString(xmlData)
        nodeName, tree = parse_element(dom)
        for rr in tree["ListResourceRecordSetsResponse"]["ResourceRecordSets"]["ResourceRecordSet"]:
            data = []
            if type (rr["ResourceRecords"]["ResourceRecord"]) == list:
                for rrSet in rr["ResourceRecords"]["ResourceRecord"]:
                    data.append(rrSet["Value"]["#text"]["data"])
            else:
                data = rr["ResourceRecords"]["ResourceRecord"]["Value"]
            resourceRecords.append({"name": rr["Name"]["#text"]["data"], "type": rr["Type"]["#text"]["data"], "TTL": rr["TTL"]["#text"]["data"], "data": data})
        return resourceRecords
    
    def updateRR(self, changes):
        change = '<?xml version="1.0" encoding="UTF-8"?><ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2012-02-29/"><ChangeBatch><Comment>Comment...</Comment><Changes>{}</Changes></ChangeBatch></ChangeResourceRecordSetsRequest>'.format(changes)
        xmlData = self.callR53(self.hostedZone + "/rrset", change)
        dom = xml.dom.minidom.parseString(xmlData)
        nodeName, tree = parse_element(dom)
        changeID = dom.getElementsByTagName("Id").item(0).firstChild.data
        changeStatus = dom.getElementsByTagName("Status").item(0).firstChild.data
        changeSubmitTime = dom.getElementsByTagName("SubmittedAt").item(0).firstChild.data
        if changeStatus == "PENDING":
           return self.waitForInSync(changeID)
        elif changeStatus == "INSYNC":
            return changeID
        
    def waitForInSync(self, changeID):
        for i in range(15):
            xmlData = self.callR53(changeID)
            dom = xml.dom.minidom.parseString(xmlData)    
            changeStatus = dom.getElementsByTagName("Status").item(0).firstChild.data
            if changeStatus == "INSYNC":
                return changeID
            time.sleep(5)
        raise ChangePending("Still waiting for change to sync: {}".format(changeID))

        
class MyIP:
    api = "dynR53PHP"
    apiKey = None
    apiurl = None
    
    def __init__(self, config):
        self.api = config["api"]
        self.apiKey = config["apiKey"]
        self.apiurl = config["apiurl"]
        
    def getMyIP(self):
        if self.api == "dynR53PHP":
            return self.dynR53PHP()
        if self.api == "simple":
            return self.simple()
        if self.api == "local":
            return self.local()
        print "Failed to get api type"
        
    def simple(self):
        http = httplib2.Http()
        response, content = http.request(self.apiurl)
        if response.status != 200:
            raise http200("HTTP returned status 200: " + content)
        return json.loads(content)

        
    def dynR53PHP(self):
        timeStamp = formatdate(timeval=None, localtime=False, usegmt=True)
        xauth = base64.b64encode(hmac.new(self.apiKey, timeStamp, hashlib.sha256).hexdigest())
        http = httplib2.Http()
        response, content = http.request(self.apiurl, headers={'Date': timeStamp, 'x-auth': xauth})
        
        if response.status != 200:
            raise http200("HTTP returned status 200: " + content)

        responseDate = response["date"]
        responseXAuth = response["x-auth"]
        xauthReply = base64.b64encode(hmac.new(self.apiKey, responseDate + content, hashlib.sha256).hexdigest())
        if responseXAuth == xauthReply:
            return json.loads(content)
        else:
            raise hmacMisMatch("Reply HMACs don't match")
    
    def local(self):
        import socket
        ip = {}
        try:
            ip["ip"] = socket.gethostbyname(socket.getfqdn())
        except:
            raise LocalIPFetchFail("Unable to determine local IP address")
        return ip
        
def parse_element(element):
    dict_data = dict()
    if element.nodeType == element.TEXT_NODE:
        dict_data['data'] = element.data
    if element.nodeType not in [element.TEXT_NODE, element.DOCUMENT_NODE, 
                                element.DOCUMENT_TYPE_NODE]:
        for item in element.attributes.items():
            dict_data[item[0]] = item[1]
    if element.nodeType not in [element.TEXT_NODE, element.DOCUMENT_TYPE_NODE]:
        for child in element.childNodes:
            child_name, child_dict = parse_element(child)
            if child_name in dict_data:
                try:
                    dict_data[child_name].append(child_dict)
                except AttributeError:
                    dict_data[child_name] = [dict_data[child_name], child_dict]
            else:
                dict_data[child_name] = child_dict 
    return element.nodeName, dict_data

config = ConfigParser.SafeConfigParser({"zoneid": None})
configFile = os.path.expanduser("~/.dynR53.conf")
if not os.path.exists(configFile):
    print "Config ({}) file does not exist!".format(configFile)
    raise SystemExit

try:
    config.read(configFile)
    key = config.get("Credentials", "key")
    id = config.get("Credentials", "id")
    r53API = config.get("Route53", "api")
    zoneName = config.get("Zone", "name")
    zoneID = config.get("Zone", "zoneid")
    zoneARR = config.get("Zone", "arr")
    zoneTxtRR = config.get("Zone", "txtrr")
    zoneTTL = config.get("Zone", "ttl")
    myIPAPI = config.get("myIP", "api")
    myIPAPIKey = config.get("myIP", "apikey")
    myIPAPIURL = config.get("myIP", "apiurl")
except ConfigParser.NoOptionError as e:
    print "Failed to load config: {}".format(e)
    raise SystemExit
except ConfigParser.NoSectionError as e:
    print "Failed to load config: {}".format(e)
    raise SystemExit
except:
    print "Unexpected error parsing config: {}".format(sys.exc_info()[0])
    raise SystemExit

aRRfqdn = "{}.{}".format(zoneARR, zoneName)
txtRRfqdn = "{}.{}".format(zoneTxtRR, zoneName)

myIPObj = MyIP({"api": myIPAPI, "apiKey": myIPAPIKey, "apiurl": myIPAPIURL})
myIP = myIPObj.getMyIP()
myIP = myIP["ip"]

r53 = callR53({"r53API": r53API, "key": key, "id": id})   

if zoneID is None:
    zoneID = r53.fetchZoneID(zoneName)

r53.hostedZone = zoneID

RRs = r53.getRRs()

aRR = None
for rr in RRs:
    if rr["type"] == "A" and rr["name"] == aRRfqdn:
        aRR = ({"name": rr["name"], "ip": rr["data"]["#text"]["data"], "ttl": rr["TTL"]})

txtRR = None
for rr in RRs:
    if rr["type"] == "TXT" and rr["name"] == txtRRfqdn:
        txtRR = ({"name": txtRRfqdn, "data": rr["data"]["#text"]["data"], "ttl": rr["TTL"]})

updateRR = None
if aRR is None:
    updateRR = True
elif myIP != aRR["ip"]:
    updateRR = True
elif zoneTTL != aRR["ttl"]:
    updateRR = True

rrChanges = ""

if updateRR is not None and aRR is not None:
    rrChanges = rrChanges + "<Change><Action>DELETE</Action><ResourceRecordSet><Name>{}</Name><Type>A</Type><TTL>{}</TTL><ResourceRecords><ResourceRecord><Value>{}</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change>".format(aRR["name"], aRR["ttl"], aRR["ip"])

if aRR is None or updateRR is not None:
    rrChanges = rrChanges + "<Change><Action>CREATE</Action><ResourceRecordSet><Name>{}</Name><Type>A</Type><TTL>{}</TTL><ResourceRecords><ResourceRecord><Value>{}</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change>".format(aRRfqdn, zoneTTL, myIP)


if txtRR is None:
    timeStamp = formatdate(timeval=None, localtime=False, usegmt=True)
    rrChanges = rrChanges + "<Change><Action>CREATE</Action><ResourceRecordSet><Name>{}</Name><Type>TXT</Type><TTL>{}</TTL><ResourceRecords><ResourceRecord><Value>\"{}\"</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change>".format(txtRRfqdn, zoneTTL, base64.b64encode(json.dumps({"updated": timeStamp, "checked-in": timeStamp})))
elif updateRR is not None:    
    rrChanges = rrChanges + "<Change><Action>DELETE</Action><ResourceRecordSet><Name>{}</Name><Type>TXT</Type><TTL>{}</TTL><ResourceRecords><ResourceRecord><Value>{}</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change>".format(txtRR["name"], txtRR["ttl"], txtRR["data"])
    timeStamp = formatdate(timeval=None, localtime=False, usegmt=True)
    rrChanges = rrChanges + "<Change><Action>CREATE</Action><ResourceRecordSet><Name>{}</Name><Type>TXT</Type><TTL>{}</TTL><ResourceRecords><ResourceRecord><Value>\"{}\"</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change>".format(txtRRfqdn, zoneTTL, base64.b64encode(json.dumps({"updated": timeStamp, "checked-in": timeStamp})))
else:
    rrChanges = rrChanges + "<Change><Action>DELETE</Action><ResourceRecordSet><Name>{}</Name><Type>TXT</Type><TTL>{}</TTL><ResourceRecords><ResourceRecord><Value>{}</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change>".format(txtRR["name"], txtRR["ttl"], txtRR["data"])
    txtDate = json.loads(base64.b64decode(txtRR["data"]))
    txtDate["checked-in"] = formatdate(timeval=None, localtime=False, usegmt=True)
    rrChanges = rrChanges + "<Change><Action>CREATE</Action><ResourceRecordSet><Name>{}</Name><Type>TXT</Type><TTL>{}</TTL><ResourceRecords><ResourceRecord><Value>\"{}\"</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change>".format(txtRRfqdn, zoneTTL, base64.b64encode(json.dumps(txtDate)))

print r53.updateRR(rrChanges)